package bibliotheque;

import java.io.Serializable;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.TreeSet;

import portes.Porte;
import portes.PorteImpl;
import reso.Client;

public class AlgoDiffusion extends AlgoAbstract{
	
	// nbPlaces : le nombre de place dans le parking
	private int nbplaces;
	// horloge locale
	private Estampille estampille;
	// les messages en attente, pending ou ready
	private ArrayList<Message> messages;
	
	// nombre d'ACK reçus
	private Integer nbAck;
	// liste des estampilles des destinataires d'une demande
	private ArrayList<Estampille> listeEstampilles;
	// thread gérant les "deliver"
	private DeliverThread deliverThread;
	
	// permet de définir un type estampille
	private static class Estampille implements Serializable{
		private int date;
		private int numero;
		
		public Estampille(int pdate, int pnumero){
			this.date = pdate;
			this.numero = pnumero;
		}
		
		public Estampille clone(){
			Estampille e = new Estampille(this.date, this.numero);
			return e;
		}
		
		public boolean estInferieur(Estampille e){
			return (this.date < e.date) || (this.date == e.date && this.numero < e.numero);
		}
		
		public String toString(){
			return this.date + "." + this.numero;
		}
	}
	
	// permet de définir un type et une estampille aux messages
	private static class Message implements Serializable{
		// Type de message ENTREE, SORTIE ou ACK
		private TypMessage type;
		private Estampille estampille;
		// l'estampille d'un message pending est amenée à être modifiée,
		// on doit donc pouvoir identifier chaque message (expediteur 
		// à l'origine du message ET numero d'ordre)
		private int expediteur;
		private int numeroordre;
		// boolean à true si le message est ready, à false s'il est pending
		// non initialisé dans le constructeur
		private boolean ready;
		
		// compte et numero() permettent de donner un numero d'ordre
		// aux messages
		private static int compte = 0;
		
		private static int numero(){
			return compte++;
		}
		
		public Message(TypMessage ptype, Estampille e, int exp) {
			this.type = ptype;
			this.estampille = e;
			this.expediteur = exp;
			this.numeroordre = numero();
		}
	}
	
	private class DeliverThread implements Runnable{
		@Override
		public void run(){
			while(true){
				try {
					synchronized(this){
						wait();	
					}
					Message premiermessage;
					// on délivre les messages ready si leur estampille est 
					// inférieure à celle du premier message encore pending
					// ou si pending est vide
					synchronized(messages){
						while(!messages.isEmpty() && messages.get(0).ready){
							premiermessage = messages.remove(0);
							if(premiermessage.type == TypMessage.ENTREE){
								// cas d'une entrée
								if(nbplaces > 0){
									nbplaces--;
									((PorteImpl) porte).log("[Porte "+ id + "] entrée de voiture ACCORDÉE par la porte "+ premiermessage.expediteur + " avec l'estampille " + premiermessage.estampille + ", reste " + nbplaces + " places");
								}
								else{
									((PorteImpl) porte).log("[Porte "+ id + "] entrée de voiture REFUSÉE par la porte "+ premiermessage.expediteur + " avec l'estampille " + premiermessage.estampille + ", reste " + nbplaces + " places");
								}
							}
							else{
								// cas d'une sortie
								if(premiermessage.type == TypMessage.SORTIE){
									nbplaces++;
									((PorteImpl) porte).log("[Porte "+ id + "] SORTIE de voiture à la porte "+ premiermessage.expediteur + " avec l'estampille " + premiermessage.estampille + ", reste " + nbplaces + " places");
								}
							}
						}
					}
					} catch (InterruptedException e) {e.printStackTrace();}
				}
		}
	}
	
	public AlgoDiffusion(Porte p) throws InterruptedException, RemoteException {
		this.porte = p;
		this.id = ((PorteImpl) p).getId();
		this.nbplaces = Porte.nombreplacesinitial;
		this.estampille = new Estampille(0, this.id);
		this.messages = new ArrayList<Message>();
		//this.ready = new ArrayList<Message>();	
		this.deliverThread = new DeliverThread();
	}
	
	/**
	 * Méthode appelée au démarrage par la Porte liée à cette instance d'AlgoDiffusion
	 */
	@Override
	public void init() throws RemoteException {
		Thread deliver = new Thread(this.deliverThread);
		deliver.start();
	}
	
	@Override
	/**
	 * Implémentation d'ABCAST du point de vue du destinataire d'un message.
	 *  
	 * @param from id de la Porte qui envoie le message.
	 * @param to id de la Porte qui reçoit le message (normalement la Porte liée à cette instance d'AlgoDiffusion !).
	 * @param msg le message reçu
	 * @throws RemoteException
	 */
	public void receiveMessage(int from, int to, Serializable msg) throws RemoteException {
		Message message = (Message) msg;
		((PorteImpl) porte).log("[Porte "+ this.id + "] réception de message | from : "+ from + " | to : " + to + " | Type : " + message.type + " | Estampille : " + message.estampille);
		// Accepte le message ENTREE ou SORTIE
		if(message.type.equals(TypMessage.ENTREE) || message.type.equals(TypMessage.SORTIE)){
			this.estampille.date++;
			
			// on renvoie à l'expéditeur un message d'ACK avec l'estampille provisoire
			sendMessage(this.id, message.estampille.numero, new Message(TypMessage.ACK, this.estampille, message.expediteur));
			// on garde le message "pending" avec sa propre estampille
			message.estampille = this.estampille.clone();
			message.ready = false;
			addOrdered(this.messages, message);
		}
		
		// Accepte le message ACK
		if(message.type.equals(TypMessage.ACK)){
			this.listeEstampilles.add(message.estampille);
			synchronized(nbAck){
				nbAck.notify();
			}
		}
		
		// Accepte le message ESTAMPILLEDEF
		if(message.type.equals(TypMessage.ESTAMPILLEDEF)){
			// on recherche le message en attente de type ENTREE ou SORTIE qui 
			// émane du même expéditeur ET qui a le bon numéro d'ordre
			Iterator<Message> iter = this.messages.iterator();
			Message msgtemp = null;
			while(iter.hasNext()){
				msgtemp = iter.next();
				// cas où l'expediteur du message ESTAMPILLEDEF est celui qui est à l'initiative du message pending
				if(msgtemp.expediteur == message.expediteur && msgtemp.numeroordre == message.numeroordre){
					break;
				}
			}
			// on supprime le message pending avec son estampille provisoire
			synchronized(messages){
				this.messages.remove(msgtemp);
				// on marque le message "ready" et on l'ajoute avec l'estampille finale
				msgtemp.estampille = message.estampille;
				msgtemp.ready = true;
				addOrdered(this.messages, msgtemp);
			}
			// on délivre si besoin
			synchronized(deliverThread){
				deliverThread.notify();	
			}
		}
	}

	@Override
	/**
	 * Envoyer un message à une autre Porte. Le message est transmis à la méthode de même
	 * nom de la Porte liée à cette instance d'AlgoDiffusion.
	 * 
	 * @param from id de la Porte qui envoie le message (la Porte liée à cette instance d'AlgoDiffusion).
	 * @param to id de la Porte à qui envoyer le message.
	 * @param msg le message envoyé
	 * @throws RemoteException
	 */
	public void sendMessage(int from, int to, Serializable msg) throws RemoteException {
		porte.sendMessage(from, to, msg);
		Message message = (Message) msg;
		((PorteImpl) porte).log("[Porte "+ this.id + "] emission de message | from : "+ from + " | to : " + to + " | Type : " + message.type + " | Estampille : " + message.estampille);
	}

	@Override
	/**
	 * Recevoir et traiter une demande d'entrée d'une voiture transmise par la Porte liée à cette instance d'AlgoDiffusion.
	 * 
	 */
	public synchronized void demandeEntree() throws RemoteException, InterruptedException {
		this.estampille.date++;
		((PorteImpl) porte).log("[Porte "+ this.id + "] Demande d'ENTRÉE de voiture");
		demandeEnvoi(TypMessage.ENTREE);
	}
	
	@Override
	/**
	 * Recevoir et traiter une demande de sortie d'une voiture transmise par la Porte liée à cette instance d'AlgoDiffusion.
	 * 
	 */
	public synchronized void demandeSortie() throws RemoteException, InterruptedException {
		this.estampille.date++;
		((PorteImpl) porte).log("[Porte "+ this.id + "] Demande de SORTIE de voiture");
		demandeEnvoi(TypMessage.SORTIE);
	}

	/**
	 * Implémentation d'ABCAST du point de vue de l'expéditeur d'une demande.
	 * 
	 */
	private void demandeEnvoi(TypMessage type) throws RemoteException, InterruptedException{
		// 1/ on envoie un message de type ENTREE ou SORTIE à toutes les portes 
		// avec une estampille provisoire
		Message msg = new Message(type, this.estampille, this.id);
		for(int i = 0 ; i < Porte.nombreportes ; i++){
			sendMessage(this.id, i, msg);
		}
		
		// 2/ on récolte les estampilles reçues en retour des autres portes
		this.nbAck = 0;
		this.listeEstampilles = new ArrayList<Estampille>();
		while(nbAck < Porte.nombreportes){
			synchronized(nbAck){
				nbAck.wait();
			}
			nbAck++;
		}
		
		// 3/ on détermine l'estampille maximale
		Estampille max = listeEstampilles.get(0);
		Estampille temp;
		for(int i = 1 ; i < Porte.nombreportes ; i++){
			temp = listeEstampilles.get(i);
			max = (max.estInferieur(temp) ? temp : max);
		}
				
		// 4/ on définit l'estampille définitive du message et on le renvoie à toutes les portes
		msg.estampille = max;
		// on change le type pour le traitement chez le destinataire
		msg.type = TypMessage.ESTAMPILLEDEF;
		for(int i = 0 ; i < Porte.nombreportes ; i++){
				sendMessage(this.id, i, msg);
		}
	}
	
	/**
	 * Permet l'ajout de messages prêts à être délivrés tout en assurant que
	 * this.ready est ordonné par ordre d'estampille croissante 
	 */
	private static void addOrdered(ArrayList<Message> l, Message m){
		int i = 0;
		// on cherche le premier indice dans l où le message a une estampille
		// supérieure à celle de m
		while(i < l.size() && l.get(i).estampille.estInferieur(m.estampille)){
				i++;
		}
		// on place le message m au bon endroit dans l
		l.add(i, m);
	}
	
	// méthode utilisée pendant le debbogage pour pouvoir consulter les estampilles stockées
	private static ArrayList<Estampille> getEstampilleList(ArrayList<Message> m){
		ArrayList<Estampille> liste = new ArrayList<Estampille>();
		for(Message msg : m){
			liste.add(msg.estampille);
		}
		return liste;
	}
}